﻿using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using Nop.Core.Data;
using Nop.Plugin.Pricing.PreciousMetals.Domain;
using System.Globalization;
using Nop.Services.Directory;
using Nop.Core.Caching;

namespace Nop.Plugin.Pricing.PreciousMetals.Services
{
    public class PreciousMetalsQuoteService : IPreciousMetalsQuoteService
    {
        private readonly IRepository<PreciousMetalsQuote> _repository;
        private readonly PreciousMetalsSettings _settings;
        private readonly ICurrencyService _currencyService;

        private readonly ICacheManager _cacheManager;
        private const string CACHE_KEY = "pricing.kitco.id{0}";

        public PreciousMetalsQuoteService(ICacheManager cacheManager, IRepository<PreciousMetalsQuote> repository, PreciousMetalsSettings settings, ICurrencyService currencyService)
        {
            this._cacheManager = cacheManager;
            this._repository = repository;
            this._settings = settings;
            this._currencyService = currencyService;
        }

        public PreciousMetalsQuote GetQuote(PreciousMetalType metalType)
        {
            string key = string.Format(CACHE_KEY, (int)metalType);

            return _cacheManager.Get(string.Format(CACHE_KEY, (int)metalType), _settings.CachePeriodInMinutes, () =>
            {
                if (_settings.LiveQuoteEnabled)
                {
                    // get the last quote from the database that is within our cache time
                    var earliestValidCacheTime = DateTime.Now.AddMinutes(-_settings.CachePeriodInMinutes);
                    var quote = _repository.Table
                        .Where(q => q.MetalType == (int)metalType && q.DateRetrieved >= earliestValidCacheTime)
                        .OrderByDescending(q => q.DateRetrieved).FirstOrDefault();

                    // if nothing current is in the database, look online.
                    if (quote == null)
                        quote = GetQuoteFromInternet(metalType);

                    // return the quote
                    return quote;
                }
                else
                {
                    var quote = _repository.Table
                        .Where(q => q.MetalType == (int)metalType)
                        .OrderByDescending(q => q.DateRetrieved).FirstOrDefault();

                    if (quote == null)
                        throw new ApplicationException("No cache quote rate for precious metal");

                    return quote;
                }
            });
        }

        private PreciousMetalsQuote GetQuoteFromInternet(PreciousMetalType metalType)
        {
            // TODO: Allow the retry count and delay to be configurable
            const int retryCount = 5; // times
            const int retryDelay = 1; // seconds

            // TODO: add other providers and make choice of provider configurable
            var quoteProvider = PreciousMetalsQuoteProvider.Kitco;

            // Retry until we get a quote or the count is exceeded
            for (var i = 0; i < retryCount; i++)
            {
                // get the quote from the provider
                PreciousMetalsQuote quote;
                switch (quoteProvider)
                {
                    case PreciousMetalsQuoteProvider.Kitco:
                        quote = GetQuoteFromKitco(metalType);
                        break;

                    // TODO: add more providers

                    default:
                        throw new InvalidOperationException("Invalid Precious Metals Quote Provider");
                }

                // if we got a quote, save it and return it
                if (quote != null)
                {
                    _repository.Insert(quote);
                    return quote;
                }

                // wait a second before trying again
                Thread.Sleep(1000 * retryDelay);
            }

            // no quote could be retrieved
            return null;
        }

        /// <summary>
        /// Gets a quote from Kitco on the Internet
        /// </summary>
        private PreciousMetalsQuote GetQuoteFromKitco(PreciousMetalType metalType)
        {
            // Kitco provides a public text-only quote page at http://www.kitco.com/texten/texten.html however this page can be difficult to parse.
            // A much cleaner result is used in the free Kitco Charts web application at http://charts.kitco.com which is the interface we use below.

            // The timestamp in the result is in Eastern time.  We'll need the TimeZoneInfo for Eastern time so we can convert to local time later.
            var easternTimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

            // Call the charts interface for the specified metal.  The enum name of the metal (as upper case) matches what Kitco expects.
            var url = string.Format("http://charts.kitco.com/KitcoCharts/RequestHandler?requestName=getSymbolSnapshot&Symbol={0}", metalType.ToString().ToUpper());
            var strResponse = GetWebSiteContent(url);
            if (strResponse == null)
                return null;

            // The response is a Base64 encoded string consisting of comma separated values.  We'll convert that to an array here.
            var strArray = Encoding.UTF8.GetString(Convert.FromBase64String(strResponse)).Split(new[] { ',' });

            // Kitco's results are always in a straight two digit number format
            var numberFormat = new NumberFormatInfo
            {
                NumberDecimalDigits = 2,
                NumberDecimalSeparator = ".",
                NumberGroupSeparator = ""
            };

            // Create the new quote from the values provided.
            // Include the DateRetrieved as a separate field in case our clocks don't match exactly with Kitco's.
            // We'll keep all data returned (even the fields we don't need) so we have an accurate representation of the quote in the database.
            var quote = new PreciousMetalsQuote
            {
                DateRetrieved = DateTime.Now,
                MetalType = (int)metalType,
                Date = TimeZoneInfo.ConvertTime(DateTime.Parse(strArray[0]), easternTimeZoneInfo, TimeZoneInfo.Local),
                Bid = decimal.Parse(strArray[1], numberFormat),
                Ask = decimal.Parse(strArray[2], numberFormat),
                Change = decimal.Parse(strArray[3], numberFormat),
                Low = decimal.Parse(strArray[4], numberFormat),
                High = decimal.Parse(strArray[5], numberFormat),
                ChangePercent = decimal.Parse(strArray[6].TrimEnd(new[] { '%' }))
            };

            // Kitco always returns US Dollars.  Convert to the primary store currency before saving the quote
            var currencyUSD = _currencyService.GetCurrencyByCode("USD");
            quote.Bid = _currencyService.ConvertToPrimaryStoreCurrency(quote.Bid, currencyUSD);
            quote.Ask = _currencyService.ConvertToPrimaryStoreCurrency(quote.Ask, currencyUSD);
            quote.Low = _currencyService.ConvertToPrimaryStoreCurrency(quote.Low, currencyUSD);
            quote.High = _currencyService.ConvertToPrimaryStoreCurrency(quote.High, currencyUSD);
            quote.Change = _currencyService.ConvertToPrimaryStoreCurrency(quote.Change, currencyUSD);

            // return the quote
            return quote;
        }

        private static string GetWebSiteContent(string url)
        {
            var request = WebRequest.Create(url);
            var response = request.GetResponse();
            try
            {
                // Get a stream from the response
                using (var stream = response.GetResponseStream())
                {
                    // The stream shouldn't be null
                    if (stream == null)
                        return null;

                    // We'll need a reader to parse the string
                    using (var reader = new StreamReader(stream))
                    {
                        // Get the response string
                        var strResponse = reader.ReadToEnd();

                        // Make sure we close everything
                        reader.Close();
                        stream.Close();
                        response.Close();

                        // return it
                        return strResponse;
                    }
                }
            }
            catch
            {
                // make sure we always close the HTTP channel.
                response.Close();

                throw;
            }
        }
    }
}